package myutil.concurrency;

import java.util.Date;
import java.util.Random;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;

public class ConnectionPool {

    public static class Connection {

        final private String url;
        final private String user;
        final private String password;
        final private int num;
        private boolean beingUsed = false;

        public Connection(String url, String user, String password, int num) {
            this.url = url;
            this.user = user;
            this.password = password;
            this.num = num;
        }

        public boolean isBeingUsed() {
            return beingUsed;
        }

        public void use() {
            beingUsed = true;
            try {
                Thread.sleep(5000);
            } catch (InterruptedException e) {
            }
            System.out.println(" Connection " + num + " is being used...");
            beingUsed = false;
        }
    }

    public static class WaitingTimeOutException extends Throwable {
    }

    private final String url;
    private final String user;
    private final String password;

    private final int N_CONNECTIONS = 5;
    private final Semaphore semaphore = new Semaphore(N_CONNECTIONS);
    private final int WAITING_TIMEOUT = 10000;
    private final int IDLE_TIMEOUT = 10000;
    private final int IDLE_CHECK_INTERVAL = 500;

    private final Connection[] connections = new Connection[N_CONNECTIONS];
    private final boolean[] busy = new boolean[N_CONNECTIONS];
    private final Date[] lastUsed = new Date[N_CONNECTIONS];

    private volatile boolean idleConnectionFinderStarted = false;

    public ConnectionPool(String url, String user, String password) {
        this.url = url;
        this.user = user;
        this.password = password;

        for (int i = 0; i < N_CONNECTIONS; i++) {
            connections[i] = new Connection(url, user, password, i);
            busy[i] = false;
            lastUsed[i] = null;
        }
    }

    private void startIdleConnectionsFinder() {
        Thread idleConnectionFinderThread = new Thread(new Runnable() {
            @Override
            public void run() {
                idleConnectionFinder();
            }
        });
        idleConnectionFinderThread.setDaemon(true);
        idleConnectionFinderThread.start();
    }

    public Connection checkOut() throws InterruptedException, WaitingTimeOutException {
        // call startidleConnectionFinder only once, 
        // when connection is checked out
        if (!idleConnectionFinderStarted) {
            synchronized (this) {
                if (!idleConnectionFinderStarted) {
                    idleConnectionFinderStarted = true;
                    startIdleConnectionsFinder();
                }
            }
        }

        if (!semaphore.tryAcquire(WAITING_TIMEOUT, TimeUnit.MILLISECONDS)) {
            throw new ConnectionPool.WaitingTimeOutException();
        }
        Connection connection = null;
        synchronized (this) {
            for (int i = 0; i < N_CONNECTIONS; i++) {
                if (!busy[i]) {
                    connection = connections[i];
                    busy[i] = true;
                    lastUsed[i] = null;
                    System.out.println("Connection " + i + " was taken from pool");
                    break;
                }
            }
        }
        return connection;
    }

    private void checkIn(int connectionNumber) {
        busy[connectionNumber] = false;
        semaphore.release();
        System.out.println("Connection " + connectionNumber + " was put into pool");
    }

    public synchronized void checkIn(Connection c) {
        for (int i = 0; i < N_CONNECTIONS; i++) {
            if (busy[i] && c.equals(connections[i])) {
                checkIn(i);
                break;
            }
        }
    }

    public void idleConnectionFinder() {
        while (true) {
            try {
                Thread.sleep(IDLE_CHECK_INTERVAL);
            } catch (InterruptedException ex) {
            }
            synchronized (this) {
                for (int i = 0; i < N_CONNECTIONS; i++) {

                    if (busy[i]) {
                        if (!connections[i].isBeingUsed() && lastUsed[i] == null) {
                            lastUsed[i] = new Date();
                        } else if (lastUsed[i] != null) {
                            long lastUsedTime = lastUsed[i].getTime();
                            if (System.currentTimeMillis() - lastUsedTime > IDLE_TIMEOUT) {
                                System.out.println("Connection " + i + " exceded IDLE_TIMEOUT");
                                checkIn(i);
                                lastUsed[i] = null;
                            }
                        }
                    }
                }
            }
        }
    }

    public static void main(String[] args) {
        final Random random = new Random();
        final ConnectionPool cp = new ConnectionPool("jdbc:postgresql://localhost/testdb", "dbuser", "dbpassword");
        ExecutorService executorService = Executors.newCachedThreadPool();
        for (int i = 0; i < 100; i++) {
//            try {
//                Thread.sleep(random.nextInt(5000));
//            } catch (InterruptedException e) {}
            executorService.submit(new Runnable() {

                @Override
                public void run() {
                    Connection connection = null;
                    while (connection == null) {
                        try {
                            connection = cp.checkOut();
                        } catch (InterruptedException e) {
                        } catch (WaitingTimeOutException e) {
                            System.out.println("WaitingTimeOutException");
                        }
                    }
                    connection.use();
                    try {
                        Thread.sleep(random.nextInt(5000));
                    } catch (InterruptedException e) {
                    }
                    cp.checkIn(connection);
                }
            });
        }
    }
}
